<?php

namespace Combodo\iTop\Test\UnitTest\Core;

use CMDBSource;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use MetaModel;

/**
 * created a dedicated test for external keys imports.
 *
 * Class BulkChangeExtKeyTest
 *
 * @package Combodo\iTop\Test\UnitTest\Core
 */
class BulkChangeExtKeyTest extends ItopDataTestCase
{
	public const CREATE_TEST_ORG = true;

	/**
	 * this test may delete Person objects to cover all usecases
	 * DO NOT CHANGE USE_TRANSACTION value to avoid any DB loss!
	 */
	public const USE_TRANSACTION = true;

	private $sUid;

	protected function setUp(): void
	{
		parent::setUp();
		require_once(APPROOT.'core/bulkchange.class.inc.php');
	}

	private function deleteAllRacks()
	{
		$oSearch = \DBSearch::FromOQL("SELECT Rack");
		$oSet = new \DBObjectSet($oSearch);
		$iCount = $oSet->Count();
		if ($iCount != 0) {
			while ($oRack = $oSet->Fetch()) {
				$oRack->DBDelete();
			}
		}
	}

	/**
	 * @dataProvider ReconciliationKeyProvider
	 */
	public function testExternalFieldIssueImportFail_NoObjectAtAll($bIsRackReconKey)
	{
		$this->deleteAllRacks();

		$this->performBulkChangeTest(
			'There are no \'Rack\' objects',
			"",
			null,
			$bIsRackReconKey
		);
	}

	public function createRackObjects($aRackDict)
	{
		foreach ($aRackDict as $iOrgId => $aRackNames) {
			foreach ($aRackNames as $sRackName) {
				$this->createObject('Rack', ['name' => $sRackName, 'description' => "{$sRackName}Desc", 'org_id' => $iOrgId]);
			}
		}
	}

	private function createAnotherUserInAnotherOrg()
	{
		$oOrg2 = $this->CreateOrganization('UnitTestOrganization2');
		$oProfile = \MetaModel::GetObjectFromOQL("SELECT URP_Profiles WHERE name = :name", ['name' => 'Configuration Manager'], true);

		$sUid = $this->GetUid();

		$oSet = new \ormLinkSet(\UserLocal::class, 'profile_list', \DBObjectSet::FromScratch(\URP_UserProfile::class));
		$oSet->AddItem(MetaModel::NewObject('URP_UserProfile', ['profileid' => $oProfile->GetKey(), 'reason' => 'UNIT Tests']));

		$oPerson = $this->CreatePerson('666', $oOrg2->GetKey());
		$oUser = $this->createObject('UserLocal', [
			'contactid' => $oPerson->GetKey(),
			'login' => $sUid,
			'password' => "ABCdef$sUid@12345",
			'language' => 'EN US',
			'profile_list' => $oSet,
		]);

		$oAllowedOrgList = $oUser->Get('allowed_org_list');
		/** @var \URP_UserOrg $oUserOrg */
		$oUserOrg = \MetaModel::NewObject('URP_UserOrg', ['allowed_org_id' => $oOrg2->GetKey(),]);
		$oAllowedOrgList->AddItem($oUserOrg);
		$oUser->Set('allowed_org_list', $oAllowedOrgList);
		$oUser->DBWrite();
		return [$oOrg2, $oUser];
	}

	public function ReconciliationKeyProvider()
	{
		return [
			'rack_id NOT a reconcilication key' => [ false ],
			'rack_id reconcilication key' => [ true ],
		];
	}

	/**
	 * @dataProvider ReconciliationKeyProvider
	 */
	public function testExternalFieldIssueImportFail_NoObjectVisibleByCurrentUser($bIsRackReconKey)
	{
		$this->deleteAllRacks();
		$this->createRackObjects(
			[
				$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4'],
			]
		);

		[$oOrg2, $oUser] = $this->createAnotherUserInAnotherOrg();
		\UserRights::Login($oUser->Get('login'));

		$this->performBulkChangeTest(
			"There are no 'Rack' objects found with your current profile",
			"",
			$oOrg2,
			$bIsRackReconKey
		);
	}

	/**
	 * @dataProvider ReconciliationKeyProvider
	 */
	public function testExternalFieldIssueImportFail_SomeObjectVisibleByCurrentUser($bIsRackReconKey)
	{
		$this->deleteAllRacks();
		[$oOrg2, $oUser] = $this->createAnotherUserInAnotherOrg();
		$this->createRackObjects(
			[
				$this->getTestOrgId() => ['RackTest1', 'RackTest2'],
				$oOrg2->GetKey() => ['RackTest3', 'RackTest4'],
			]
		);

		\UserRights::Login($oUser->Get('login'));

		$this->performBulkChangeTest(
			"There are some 'Rack' objects not visible with your current profile",
			"Some possible 'Rack' value(s): RackTest3, RackTest4",
			$oOrg2,
			$bIsRackReconKey
		);
	}

	/**
	 * @dataProvider ReconciliationKeyProvider
	 */
	public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser($bIsRackReconKey)
	{
		$this->deleteAllRacks();
		$this->createRackObjects(
			[
				$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4'],
			]
		);

		$this->performBulkChangeTest(
			"No match for value 'UnexistingRack'",
			"Some possible 'Rack' value(s): RackTest1, RackTest2, RackTest3...",
			null,
			$bIsRackReconKey
		);
	}

	/**
	 * @dataProvider ReconciliationKeyProvider
	 */
	public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser_AmbigousMatch($bIsRackReconKey)
	{
		$this->deleteAllRacks();
		$this->createRackObjects(
			[
				$this->getTestOrgId() => ['UnexistingRack', 'UnexistingRack'],
			]
		);

		$this->performBulkChangeTest(
			"Invalid value for attribute",
			"Ambiguous: found 2 objects",
			null,
			$bIsRackReconKey,
			null,
			null,
			null,
			null,
			'Found 2 matches'
		);
	}

	/**
	 * @dataProvider ReconciliationKeyProvider
	 */
	public function testExternalFieldIssueImportFail_AllObjectsVisibleByCurrentUser_FurtherExtKeyForRack($bIsRackReconKey)
	{
		$this->deleteAllRacks();
		$this->createRackObjects(
			[
				$this->getTestOrgId() => ['RackTest1', 'RackTest2', 'RackTest3', 'RackTest4'],
			]
		);

		$aCsvData = [["UnexistingRackDescription"]];
		$aExtKeys = ["org_id" => ["name" => 0], "rack_id" => ["name" => 1, "description" => 3]];

		$sSearchLinkUrl = 'UI.php?operation=search&filter='.\rawurlencode('%5B%22SELECT+%60Rack%60+FROM+Rack+AS+%60Rack%60+WHERE+%28%28%60Rack%60.%60name%60+%3D+%3Aname%29+AND+%28%60Rack%60.%60description%60+%3D+%3Adescription%29%29%22%2C%7B%22name%22%3A%22UnexistingRack%22%2C%22description%22%3A%22UnexistingRackDescription%22%7D%2C%5B%5D%5D');
		$this->performBulkChangeTest(
			"No match for value 'UnexistingRack UnexistingRackDescription'",
			"Some possible 'Rack' value(s): RackTest1 RackTest1Desc, RackTest2 RackTest2Desc, RackTest3 RackTest3Desc...",
			null,
			$bIsRackReconKey,
			$aCsvData,
			[],
			$aExtKeys,
			$sSearchLinkUrl
		);
	}

	private function GetUid()
	{
		if (is_null($this->sUid)) {
			$this->sUid = uniqid('test');
		}

		return $this->sUid;
	}

	public function performBulkChangeTest(
		$sExpectedDisplayableValue,
		$sExpectedDescription,
		$oOrg,
		$bIsRackReconKey,
		$aAdditionalCsvData = null,
		$aAdditionalAttributes = null,
		$aExtKeys = null,
		$sSearchLinkUrl = null,
		$sError = "Object not found"
	) {
		if ($sSearchLinkUrl === null) {
			$sSearchLinkUrl = 'UI.php?operation=search&filter='.rawurlencode('%5B%22SELECT+%60Rack%60+FROM+Rack+AS+%60Rack%60+WHERE+%28%60Rack%60.%60name%60+%3D+%3Aname%29%22%2C%7B%22name%22%3A%22UnexistingRack%22%7D%2C%5B%5D%5D');
		}
		if (is_null($oOrg)) {
			$iOrgId = $this->getTestOrgId();
			$sOrgName = "UnitTestOrganization";
		} else {
			$iOrgId = $oOrg->GetKey();
			$sOrgName = $oOrg->Get('name');
		}

		$sUid = $this->GetUid();

		$aCsvData = [[$sOrgName, "UnexistingRack", "$sUid"]];
		if ($aAdditionalCsvData !== null) {
			foreach ($aAdditionalCsvData as $i => $aData) {
				foreach ($aData as $sData) {
					$aCsvData[$i][] = $sData;
				}
			}
		}
		$aAttributes = ["name" => 2];
		if ($aAdditionalAttributes !== null) {
			$aAttributes = array_merge($aAttributes, $aAdditionalAttributes);
		}
		if ($aExtKeys == null) {
			$aExtKeys = ["org_id" => ["name" => 0], "rack_id" => ["name" => 1]];
		}
		$aReconcilKeys = [ "name" ];

		$aResult = [
			0 => $sOrgName,
			"org_id" => $iOrgId,
			1 => "UnexistingRack",
			2 => "\"$sUid\"",
			"rack_id" => [
				$sExpectedDisplayableValue,
				$sExpectedDescription,
			],
			"__STATUS__" => "Issue: Unexpected attribute value(s)",
			"__ERRORS__" => $sError,
		];

		if ($bIsRackReconKey) {
			$aReconcilKeys[] = "rack_id";
			$aResult[2] = $sUid;
			$aResult["__STATUS__"] = "Issue: failed to reconcile";
		}

		CMDBSource::Query('START TRANSACTION');
		try {

			//change value during the test
			$db_core_transactions_enabled = MetaModel::GetConfig()->Get('db_core_transactions_enabled');
			MetaModel::GetConfig()->Set('db_core_transactions_enabled', false);

			$this->debug("aCsvData:".json_encode($aCsvData[0]));
			$this->debug("aReconcilKeys:".var_export($aReconcilKeys));
			$oBulk = new \BulkChange(
				"Server",
				$aCsvData,
				$aAttributes,
				$aExtKeys,
				$aReconcilKeys,
				null,
				null,
				"Y-m-d H:i:s", // date format
				true // localize
			);
			$this->debug("BulkChange:");
			$oChange = \CMDBObject::GetCurrentChange();
			$this->debug("GetCurrentChange:");
			$aRes = $oBulk->Process($oChange);
			$this->debug("Process:");
			static::assertNotNull($aRes);
			$this->debug("assertNotNull:");
			foreach ($aRes as $aRow) {
				if (array_key_exists('__STATUS__', $aRow)) {
					$sStatus = $aRow['__STATUS__'];
					$this->debug("sStatus:".$sStatus->GetDescription());
					$this->assertEquals($aResult["__STATUS__"], $sStatus->GetDescription());
					foreach ($aRow as $i => $oCell) {
						if ($i != "finalclass" && $i != "__STATUS__" && $i != "__ERRORS__") {
							$this->debug("i:".$i);
							if (array_key_exists($i, $aResult)) {
								$this->debug("aResult:".var_export($aResult[$i]));
								if ($oCell instanceof \CellStatus_SearchIssue ||
									$oCell instanceof \CellStatus_Ambiguous) {
									$this->assertEquals(
										$aResult[$i][0],
										$oCell->GetCLIValue(),
										"failure on ".get_class($oCell).' cell type'
									);
									$this->assertEquals(
										$sSearchLinkUrl,
										$oCell->GetSearchLinkUrl(),
										"failure on ".get_class($oCell).' cell type'
									);
									$this->assertEquals(
										$aResult[$i][1],
										$oCell->GetDescription(),
										"failure on ".get_class($oCell).' cell type'
									);
								}
							}
						} elseif ($i === "__ERRORS__") {
							$sErrors = array_key_exists("__ERRORS__", $aResult) ? $aResult["__ERRORS__"] : "";
							$this->assertEquals($sErrors, $oCell->GetDescription());
						}
					}
					$this->assertEquals($aResult[0], $aRow[0]->GetCLIValue());
				}
			}
			MetaModel::GetConfig()->Set('db_core_transactions_enabled', $db_core_transactions_enabled);
		} finally {
			CMDBSource::Query('ROLLBACK');
		}
	}
}
